/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.dspot.declex.action;

import com.dspot.declex.annotation.action.ActionFor;
import com.dspot.declex.annotation.action.Assignable;
import com.dspot.declex.annotation.action.Field;
import com.dspot.declex.annotation.action.FormattedExpression;
import com.dspot.declex.annotation.action.Literal;
import com.dspot.declex.annotation.action.StopOn;
import com.dspot.declex.api.action.process.ActionInfo;
import com.dspot.declex.api.action.process.ActionMethod;
import com.dspot.declex.api.action.process.ActionMethodParam;
import com.dspot.declex.api.action.process.ActionProcessor;
import com.dspot.declex.util.DeclexConstant;
import com.dspot.declex.util.TypeUtils;
import com.helger.jcodemodel.AbstractJClass;
import com.helger.jcodemodel.JDefinedClass;
import com.helger.jcodemodel.JMethod;
import com.helger.jcodemodel.JMod;

import org.ohosannotations.Option;
import org.ohosannotations.api.view.HasViews;
import org.ohosannotations.helper.APTCodeModelHelper;
import org.ohosannotations.helper.ClassesHolder;
import org.ohosannotations.helper.IdAnnotationHelper;
import org.ohosannotations.internal.InternalOhosAnnotationsEnvironment;
import org.ohosannotations.internal.helper.OhosManifestFinder;
import org.ohosannotations.logger.Logger;
import org.ohosannotations.logger.LoggerFactory;

import java.lang.annotation.Annotation;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.tools.JavaFileObject;

import static com.helger.jcodemodel.JExpr._new;

/**
 * 行动
 *
 * @author Administrator
 * @date 2021/07/27
 * @since 2021-07-13
 */
public class Actions {

    /**
     * 选择调试操作
     */
    public static final Option OPTION_DEBUG_ACTIONS = new Option("debugActions", "false");

    /**
     * 日志记录器
     */
    protected static final Logger LOGGER = LoggerFactory.getLogger(Actions.class);

    /**
     * 实例
     */
    private static Actions instance;

    /**
     * 类装入器的处理器
     */
    private final Map<String, ClassLoader> classLoaderForProcessor = new HashMap<>();

    /**
     * 行动持有者
     *///<Action Holder Class, Is Exported (Declared in a library)
    private final Map<String, Boolean> ACTION_HOLDERS = new HashMap<>();
    /**
     * 行动行动持有人元素
     */
    private final Map<String, TypeElement> ACTION_HOLDER_ELEMENT_FOR_ACTION = new HashMap<>();

    /**
     * 行动注释
     */
    public final static Set<Class<? extends Annotation>> ACTION_ANNOTATION = new HashSet<>();
    /**
     * 外部行为
     */
    private final Set<String> EXTERNAL_ACTIONS = new HashSet<>();

    /**
     * 动作名称
     */
    private final Map<String, String> ACTION_NAMES = new HashMap<>();
    /**
     * 行动信息
     */
    private final Map<String, ActionInfo> ACTION_INFOS = new HashMap<>();

    /**
     * env
     */
    private InternalOhosAnnotationsEnvironment env;

    /**
     * 生成在圆形的
     */
    private boolean generateInRound = true;

    /**
     * 注释的助手
     */
    private final IdAnnotationHelper annotationHelper;
    /**
     * 代码模型辅助
     */
    private final APTCodeModelHelper codeModelHelper;

    /**
     * 获得实例
     *
     * @return {@link Actions}
     */
    public static Actions getInstance() {
        return instance;
    }

    /**
     * 行动
     *
     * @param env env
     */
    public Actions(InternalOhosAnnotationsEnvironment env) {

        this.env = env;

        ACTION_ANNOTATION.add(Assignable.class);
        ACTION_ANNOTATION.add(Field.class);
        ACTION_ANNOTATION.add(FormattedExpression.class);
        ACTION_ANNOTATION.add(Literal.class);
        ACTION_ANNOTATION.add(StopOn.class);

        annotationHelper = new IdAnnotationHelper(env, ActionFor.class.getCanonicalName());
        codeModelHelper = new APTCodeModelHelper(env);

        Actions.instance = this;
    }

    /**
     * 是行动
     *
     * @param name 的名字
     * @return boolean
     */
    public boolean isAction(String name) {

        String actionToCheck = name.substring(0, name.lastIndexOf('.'));
        if (actionToCheck.isEmpty() || !actionToCheck.endsWith(".Action")) return false;

//        if (actionToCheck.equals(DeclexConstant.ACTION)) return true;
        if (actionToCheck.equals(env.getOhosManifest().getApplicationPackage()+ ".Action")) return true;

        TypeElement element = env.getProcessingEnvironment().getElementUtils().getTypeElement(actionToCheck);
        if (element == null) return false;

        boolean isAction = element.getAnnotation(com.dspot.declex.annotation.action.Actions.class) != null;
        if (isAction) {
            //If it is an Actions container object, add it as an export Action
            addActions(actionToCheck);
        }

        return isAction;

    }

    /**
     * 添加操作
     *
     * @param actions 行动
     */
    private void addActions(String actions) {

        if (!EXTERNAL_ACTIONS.contains(actions)) {
            TypeElement typeElement = env.getProcessingEnvironment().getElementUtils().getTypeElement(actions);

            CLASSES:
            for (Element element : typeElement.getEnclosedElements()) {

                if (element.getKind().isClass()) {

                    List<? extends TypeMirror> superTypesForGate = env.getProcessingEnvironment().getTypeUtils().directSupertypes(element.asType());
                    for (TypeMirror gate : superTypesForGate) {
                        TypeElement superElementForGate = env.getProcessingEnvironment().getElementUtils().getTypeElement(gate.toString());
                        if (superElementForGate == null) continue;
                        if (superElementForGate.getKind().equals(ElementKind.INTERFACE)) continue;
                        if (superElementForGate.asType().toString().equals(Object.class.getCanonicalName())) continue;

                        //This is the Gate element, its parent it is the Holder
                        List<? extends TypeMirror> superTypesForHolder = env.getProcessingEnvironment().getTypeUtils().directSupertypes(superElementForGate.asType());
                        for (TypeMirror holder : superTypesForHolder) {
                            TypeElement superElementForHolder = env.getProcessingEnvironment().getElementUtils().getTypeElement(holder.toString());
                            if (superElementForHolder == null) continue;
                            if (superElementForHolder.getKind().equals(ElementKind.INTERFACE)) continue;
                            if (superElementForHolder.asType().toString().equals(Object.class.getCanonicalName()))
                                continue;

                            addActionHolder(superElementForHolder.asType().toString(), true);

                            //This is the Holder element
                            continue CLASSES;
                        }
                    }

                }

            }

            EXTERNAL_ACTIONS.add(actions);
        }

    }

    /**
     * 添加动作持有人
     *
     * @param action 行动
     */
    public void addActionHolder(String action) {
        this.addActionHolder(action, false);
    }

    /**
     * 添加动作持有人
     *
     * @param actionHolder 行动持有人
     * @param isExternal 是外部
     */
    private void addActionHolder(String actionHolder, boolean isExternal) {
        ACTION_HOLDERS.put(actionHolder, isExternal);
        createInformationForAction(actionHolder, isExternal);
    }

    /**
     * 行动保持者行动
     *
     * @param action 行动
     * @return {@link TypeElement}
     */
    public TypeElement getActionHolderForAction(String action) {
        return ACTION_HOLDER_ELEMENT_FOR_ACTION.get(action);
    }

    /**
     * 已经行动命名为
     *
     * @param actionName 动作名称
     * @return boolean
     */
    public boolean hasActionNamed(String actionName) {
        return ACTION_NAMES.containsKey(actionName);
    }

    /**
     * 获得动作名称
     *
     * @return {@link Map<String, String>}
     */
    public Map<String, String> getActionNames() {
        return Collections.unmodifiableMap(ACTION_NAMES);
    }

    /**
     * 让行动信息
     *
     * @return {@link Map<String, ActionInfo>}
     */
    public Map<String, ActionInfo> getActionInfos() {
        return Collections.unmodifiableMap(ACTION_INFOS);
    }

    /**
     * 添加操作
     *
     * @param name 的名字
     * @param clazz clazz
     * @param info 信息
     */
    public void addAction(String name, String clazz, ActionInfo info) {
        addAction(name, clazz, info, true);
    }

    /**
     * 添加操作
     *
     * @param name 的名字
     * @param clazz clazz
     * @param info 信息
     * @param stopGeneration 停止代
     */
    private void addAction(String name, String clazz, ActionInfo info, boolean stopGeneration) {
        if (stopGeneration) {
            this.generateInRound = false;
        }

        ACTION_NAMES.put("$" + name, clazz);

        ActionInfo prevInfo = ACTION_INFOS.get(clazz);
        if (prevInfo != null) info.setReferences(prevInfo.references);

        ACTION_INFOS.put(clazz, info);
    }

    /**
     * 创建信息操作
     *
     * @param actionHolder 行动持有人
     * @param isExternal 是外部
     */
    private void createInformationForAction(String actionHolder, boolean isExternal) {

        TypeElement typeElement = env.getProcessingEnvironment().getElementUtils().getTypeElement(actionHolder);
        TypeElement generatedHolder = env.getProcessingEnvironment().getElementUtils().getTypeElement(
            TypeUtils.getGeneratedClassName(typeElement, env)
        );

        ActionFor actionForAnnotation = null;
        try {
            actionForAnnotation = typeElement.getAnnotation(ActionFor.class);
        } catch (Exception e) {
            LOGGER.error("An error occurred processing the @ActionFor annotation", e);
        }

        if (actionForAnnotation != null) {

            for (String name : actionForAnnotation.value()) {

                ACTION_HOLDER_ELEMENT_FOR_ACTION.put("$" + name, typeElement);

                //Get model info
                final ActionInfo actionInfo = new ActionInfo(actionHolder);
                actionInfo.isGlobal = actionForAnnotation.global();
                actionInfo.isTimeConsuming = actionForAnnotation.timeConsuming();

                if (isExternal) {
                    actionInfo.generated = false;
                }

                //This will work only for cached classes
                if (generatedHolder != null) {
                    for (Element elem : generatedHolder.getEnclosedElements()) {
                        if (elem instanceof ExecutableElement) {
                            final String elemName = elem.getSimpleName().toString();
                            final List<? extends VariableElement> params = ((ExecutableElement) elem).getParameters();

                            if (elemName.equals("onViewChanged") && params.size() == 1
                                && params.get(0).asType().toString().equals(HasViews.class.getCanonicalName())) {
                                actionInfo.handleViewChanges = true;
                                break;
                            }
                        }
                    }
                }

                addAction(name, actionHolder, actionInfo, false);

                String javaDoc = env.getProcessingEnvironment().getElementUtils().getDocComment(typeElement);
                actionInfo.setReferences(javaDoc);

                List<DeclaredType> processors = annotationHelper.extractAnnotationClassArrayParameter(
                    typeElement, ActionFor.class.getCanonicalName(), "processors"
                );

                //Load processors
                if (processors != null) {
                    for (DeclaredType processor : processors) {

                        Class<ActionProcessor> processorClass = null;

                        try {

                            ClassLoader loader = classLoaderForProcessor.get(processor.toString());
                            if (loader != null) {
                                processorClass = (Class<ActionProcessor>) Class.forName(processor.toString(), true, loader);
                            } else {
                                processorClass = (Class<ActionProcessor>) Class.forName(processor.toString());
                            }

                        } catch (ClassNotFoundException e) {

                            Element element = env.getProcessingEnvironment().getElementUtils().getTypeElement(processor.toString());
                            if (element == null) {
                                LOGGER.warn("Processor \"" + processor.toString() + "\" couldn't be loaded", typeElement);
                            } else {

                                try {
                                    //Get the file from which the class was loaded
                                    java.lang.reflect.Field field = element.getClass().getField("classfile");
                                    field.setAccessible(true);
                                    JavaFileObject classfile = (JavaFileObject) field.get(element);

                                    String jarUrl = classfile.toUri().toURL().toString();
                                    jarUrl = jarUrl.substring(0, jarUrl.lastIndexOf('!') + 2);

                                    //Create or use a previous created class loader for the given file
                                    ClassLoader loader;
                                    if (classLoaderForProcessor.containsKey(jarUrl)) {
                                        loader = classLoaderForProcessor.get(jarUrl);
                                    } else {
                                        loader = new URLClassLoader(new URL[]{new URL(jarUrl)}, Actions.class.getClassLoader());
                                        classLoaderForProcessor.put(processor.toString(), loader);
                                        classLoaderForProcessor.put(jarUrl, loader);
                                    }

                                    processorClass = (Class<ActionProcessor>) Class.forName(processor.toString(), true, loader);

                                } catch (Throwable e1) {
                                    LOGGER.error("Processor \"" + processor.toString() + "\" couldn't be loaded: " + e1.getMessage(), typeElement);
                                }

                            }

                        } catch (ClassCastException e) {
                            LOGGER.error("Processor \"" + processor.toString() + "\" is not an Action Processor", typeElement);
                        }

                        if (processorClass != null) {
                            try {
                                actionInfo.processors.add(processorClass.newInstance());
                            } catch (Throwable e) {
                                LOGGER.info("Processor \"" + processor.toString() + "\" couldn't be instantiated", typeElement);
                            }
                        }

                    }
                }

                createInformationForMethods(typeElement, actionInfo);
            }

        }

    }

    /**
     * 创建信息方法
     *
     * @param typeElement 类型元素
     * @param actionInfo 行动信息
     */
    public void createInformationForMethods(Element typeElement, ActionInfo actionInfo) {
        this.createInformationForMethods(typeElement, actionInfo, null);
    }

    /**
     * 创建信息方法
     *
     * @param typeElement 类型元素
     * @param actionInfo 行动信息
     * @param methodsHandled 方法处理
     */
    private void createInformationForMethods(Element typeElement, ActionInfo actionInfo, List<String> methodsHandled) {

        if (methodsHandled == null) {
            methodsHandled = new LinkedList<>();
        }

        for (Element elem : typeElement.getEnclosedElements()) {

            if (elem.getKind() == ElementKind.METHOD) {
                if (methodsHandled.contains(elem.toString())) continue;

                final ExecutableElement element = (ExecutableElement) elem;

                List<ActionMethodParam> params = new LinkedList<>();
                for (VariableElement param : element.getParameters()) {

                    List<Annotation> annotations = new LinkedList<>();
                    for (Class<? extends Annotation> annotation : ACTION_ANNOTATION) {
                        Annotation containedAnnotation = param.getAnnotation(annotation);
                        if (containedAnnotation != null) {
                            annotations.add(containedAnnotation);
                        }
                    }

                    final AbstractJClass paramType = codeModelHelper.elementTypeToJClass(param);

                    ActionMethodParam actionMethodParam =
                        new ActionMethodParam(
                            param.getSimpleName().toString(),
                            paramType,
                            annotations
                        );
                    params.add(actionMethodParam);
                }

                List<Annotation> annotations = new LinkedList<>();
                for (Class<? extends Annotation> annotation : ACTION_ANNOTATION) {
                    Annotation containedAnnotation = element.getAnnotation(annotation);
                    if (containedAnnotation != null) {
                        annotations.add(containedAnnotation);
                    }
                }

                String javaDoc = env.getProcessingEnvironment().getElementUtils().getDocComment(element);

                final String clazz = element.getReturnType().toString();

                actionInfo.addMethod(
                    element.getSimpleName().toString(),
                    clazz,
                    javaDoc,
                    params,
                    annotations
                );

                methodsHandled.add(element.toString());
            }
        }

        List<? extends TypeMirror> superTypes = env.getProcessingEnvironment().getTypeUtils().directSupertypes(typeElement.asType());
        for (TypeMirror type : superTypes) {
            TypeElement superElement = env.getProcessingEnvironment().getElementUtils().getTypeElement(type.toString());
            if (superElement == null) continue;
            if (superElement.getKind().equals(ElementKind.INTERFACE)) continue;
            if (superElement.asType().toString().equals(Object.class.getCanonicalName())) continue;
            createInformationForMethods(superElement, actionInfo, methodsHandled);
        }

    }

    /**
     * 获取行为信息
     */
    public void getActionsInformation() {

        //This will ensure a correct working-flow for Actions Processing
        if (env.getClassesHolder() == null) {
            ClassesHolder classesHolder = new ClassesHolder(env.getProcessingEnvironment());
            env.setClassesHolder(classesHolder);
        }

        for (Entry<String, Boolean> holder : ACTION_HOLDERS.entrySet()) {
            createInformationForAction(holder.getKey(), holder.getValue());
        }
    }

    /**
     * 构建操作对象
     *
     * @return boolean
     */
    public boolean buildActionsObject() {

        if (!generateInRound) {
            generateInRound = true;
            return false;
        }

        try {
            //It is important to update the Action information again when generating
            getActionsInformation();

            //JDefinedClass Action = env.getCodeModel()._getClass(DeclexConstant.ACTION);两个module处理注解，会编译出相同的类
            String s_action = env.getOhosManifest().getApplicationPackage() + ".Action";
            JDefinedClass Action = env.getCodeModel()._getClass(s_action);
            if (Action == null) {
                Action = env.getCodeModel()._class(s_action);
                Action.annotate(com.dspot.declex.annotation.action.Actions.class);
                for (Map.Entry<String, String> entry : ACTION_NAMES.entrySet()) {

                    final String action = ACTION_NAMES.get(entry.getKey());
                    final ActionInfo actionInfo = ACTION_INFOS.get(action);

                    if (!actionInfo.generated) continue;

                    List<ActionMethod> builds = actionInfo.methods.get("build");
                    if (builds != null && builds.size() > 0) {

                        final String pkg = actionInfo.holderClass.substring(0, actionInfo.holderClass.lastIndexOf('.'));
                        JDefinedClass ActionGate = Action._class(JMod.PUBLIC | JMod.STATIC, entry.getKey());
                        ActionGate._extends(env.getJClass(pkg + "." + entry.getKey().substring(1) + "Gate"));

                        if (actionInfo.references != null) {
                            ActionGate.javadoc().add(actionInfo.references);
                        }

                        //Create the init methods for the action
                        List<ActionMethod> inits = actionInfo.methods.get("init");
                        if (inits != null) {
                            for (ActionMethod actionMethod : inits) {
                                JMethod method = Action.method(
                                    JMod.STATIC | JMod.PUBLIC, ActionGate, entry.getKey()
                                );

                                if (actionInfo.references != null) {
                                    method.javadoc().add("<br><hr><br>\n" + actionInfo.references.trim());
                                }

                                if (actionMethod.javaDoc != null) {
                                    method.javadoc().add("\n" + actionMethod.javaDoc.trim());
                                }

                                for (ActionMethodParam param : actionMethod.params) {
                                    method.param(param.clazz, param.name);
                                }

                                method.body()._return(_new(ActionGate));

                            }
                        }
                    }

                }
            }

            return true;

        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

}

